/**
 * Copyright (c) 2004-2011 QOS.ch
 * All rights reserved.
 *
 * Permission is hereby granted, free  of charge, to any person obtaining
 * a  copy  of this  software  and  associated  documentation files  (the
 * "Software"), to  deal in  the Software without  restriction, including
 * without limitation  the rights to  use, copy, modify,  merge, publish,
 * distribute,  sublicense, and/or sell  copies of  the Software,  and to
 * permit persons to whom the Software  is furnished to do so, subject to
 * the following conditions:
 *
 * The  above  copyright  notice  and  this permission  notice  shall  be
 * included in all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
 * EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
 * MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
/**
 * 
 */
package org.slf4j.instrumentation;

import static org.slf4j.helpers.MessageFormatter.format;

import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.CtField;
import javassist.NotFoundException;

import org.slf4j.helpers.MessageFormatter;

/**
 * <p>
 * LogTransformer does the work of analyzing each class, and if appropriate add
 * log statements to each method to allow logging entry/exit.
 * </p>
 * <p>
 * This class is based on the article <a href="http://today.java.net/pub/a/today/2008/04/24/add-logging-at-class-load-time-with-instrumentation.html"
 * >Add Logging at Class Load Time with Java Instrumentation</a>.
 * </p>
 */
public class LogTransformer implements ClassFileTransformer {

  /**
   * Builder provides a flexible way of configuring some of many options on the
   * parent class instead of providing many constructors.
   * 
   * {@link http
   * ://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html}
   * 
   */
  public static class Builder {

    /**
     * Build and return the LogTransformer corresponding to the options set in
     * this Builder.
     * 
     * @return
     */
    public LogTransformer build() {
      if (verbose) {
        System.err.println("Creating LogTransformer");
      }
      return new LogTransformer(this);
    }

    boolean addEntryExit;

    /**
     * Should each method log entry (with parameters) and exit (with parameters
     * and returnvalue)?
     * 
     * @param b
     *          value of flag
     * @return
     */
    public Builder addEntryExit(boolean b) {
      addEntryExit = b;
      return this;
    }

    boolean addVariableAssignment;

    // private Builder addVariableAssignment(boolean b) {
    // System.err.println("cannot currently log variable assignments.");
    // addVariableAssignment = b;
    // return this;
    // }

    boolean verbose;

    /**
     * Should LogTransformer be verbose in what it does? This currently list the
     * names of the classes being processed.
     * 
     * @param b
     * @return
     */
    public Builder verbose(boolean b) {
      verbose = b;
      return this;
    }

    String[] ignore = { "org/slf4j/", "ch/qos/logback/", "org/apache/log4j/" };

    public Builder ignore(String[] strings) {
      this.ignore = strings;
      return this;
    }

    private String level = "info";

    public Builder level(String level) {
      level = level.toLowerCase();
      if (level.equals("info") || level.equals("debug")
          || level.equals("trace")) {
        this.level = level;
      } else {
        if (verbose) {
          System.err.println("level not info/debug/trace : " + level);
        }
      }
      return this;
    }
  }

  private String level;
  private String levelEnabled;

  private LogTransformer(Builder builder) {
    String s = "WARNING: javassist not available on classpath for javaagent, log statements will not be added";
    try {
      if (Class.forName("javassist.ClassPool") == null) {
        System.err.println(s);
      }
    } catch (ClassNotFoundException e) {
      System.err.println(s);
    }

    this.addEntryExit = builder.addEntryExit;
    // this.addVariableAssignment = builder.addVariableAssignment;
    this.verbose = builder.verbose;
    this.ignore = builder.ignore;
    this.level = builder.level;
    this.levelEnabled = "is" + builder.level.substring(0, 1).toUpperCase()
        + builder.level.substring(1) + "Enabled";
  }

  private boolean addEntryExit;
  // private boolean addVariableAssignment;
  private boolean verbose;
  private String[] ignore;

  public byte[] transform(ClassLoader loader, String className, Class<?> clazz,
      ProtectionDomain domain, byte[] bytes) {

    try {
      return transform0(className, clazz, domain, bytes);
    } catch (Exception e) {
      System.err.println("Could not instrument " + className);
      e.printStackTrace();
      return bytes;
    }
  }

  /**
   * transform0 sees if the className starts with any of the namespaces to
   * ignore, if so it is returned unchanged. Otherwise it is processed by
   * doClass(...)
   * 
   * @param className
   * @param clazz
   * @param domain
   * @param bytes
   * @return
   */

  private byte[] transform0(String className, Class<?> clazz,
      ProtectionDomain domain, byte[] bytes) {

    try {
      for (int i = 0; i < ignore.length; i++) {
        if (className.startsWith(ignore[i])) {
          return bytes;
        }
      }
      String slf4jName = "org.slf4j.LoggerFactory";
      try {
        if (domain != null && domain.getClassLoader() != null) {
          domain.getClassLoader().loadClass(slf4jName);
        } else {
          if (verbose) {
            System.err.println("Skipping " + className
                + " as it doesn't have a domain or a class loader.");
          }
          return bytes;
        }
      } catch (ClassNotFoundException e) {
        if (verbose) {
          System.err.println("Skipping " + className
              + " as slf4j is not available to it");
        }
        return bytes;
      }
      if (verbose) {
        System.err.println("Processing " + className);
      }
      return doClass(className, clazz, bytes);
    } catch (Throwable e) {
      System.out.println("e = " + e);
      return bytes;
    }
  }

  private String loggerName;

  /**
   * doClass() process a single class by first creates a class description from
   * the byte codes. If it is a class (i.e. not an interface) the methods
   * defined have bodies, and a static final logger object is added with the
   * name of this class as an argument, and each method then gets processed with
   * doMethod(...) to have logger calls added.
   * 
   * @param name
   *          class name (slashes separate, not dots)
   * @param clazz
   * @param b
   * @return
   */
  private byte[] doClass(String name, Class<?> clazz, byte[] b) {
    ClassPool pool = ClassPool.getDefault();
    CtClass cl = null;
    try {
      cl = pool.makeClass(new ByteArrayInputStream(b));
      if (cl.isInterface() == false) {

        loggerName = "_____log";

        // We have to declare the log variable.

        String pattern1 = "private static org.slf4j.Logger {};";
        String loggerDefinition = format(pattern1, loggerName).getMessage();
        CtField field = CtField.make(loggerDefinition, cl);

        // and assign it the appropriate value.

        String pattern2 = "org.slf4j.LoggerFactory.getLogger({}.class);";
        String replace = name.replace('/', '.');
        String getLogger = format(pattern2, replace).getMessage();

        cl.addField(field, getLogger);

        // then check every behaviour (which includes methods). We are
        // only
        // interested in non-empty ones, as they have code.
        // NOTE: This will be changed, as empty methods should be
        // instrumented too.

        CtBehavior[] methods = cl.getDeclaredBehaviors();
        for (int i = 0; i < methods.length; i++) {
          if (methods[i].isEmpty() == false) {
            doMethod(methods[i]);
          }
        }
        b = cl.toBytecode();
      }
    } catch (Exception e) {
      System.err.println("Could not instrument " + name + ", " + e);
      e.printStackTrace(System.err);
    } finally {
      if (cl != null) {
        cl.detach();
      }
    }
    return b;
  }

  /**
   * process a single method - this means add entry/exit logging if requested.
   * It is only called for methods with a body.
   * 
   * @param method
   *          method to work on
   * @throws NotFoundException
   * @throws CannotCompileException
   */
  private void doMethod(CtBehavior method) throws NotFoundException,
      CannotCompileException {

    String signature = JavassistHelper.getSignature(method);
    String returnValue = JavassistHelper.returnValue(method);

    if (addEntryExit) {
      String messagePattern = "if ({}.{}()) {}.{}(\">> {}\");";
      Object[] arg1 = new Object[] { loggerName, levelEnabled, loggerName,
          level, signature };
      String before = MessageFormatter.arrayFormat(messagePattern, arg1)
          .getMessage();
      // System.out.println(before);
      method.insertBefore(before);

      String messagePattern2 = "if ({}.{}()) {}.{}(\"<< {}{}\");";
      Object[] arg2 = new Object[] { loggerName, levelEnabled, loggerName,
          level, signature, returnValue };
      String after = MessageFormatter.arrayFormat(messagePattern2, arg2)
          .getMessage();
      // System.out.println(after);
      method.insertAfter(after);
    }
  }
}